]> dgit.raspbian.org Git - gnome-shell.git/commitdiff
Port the EdgeDragAction to ClutterGesture
authorJonas Dreßler <verdre@v0yd.nl>
Fri, 18 Mar 2022 15:25:38 +0000 (16:25 +0100)
committerCarlos Garnacho <carlosg@gnome.org>
Thu, 28 Aug 2025 17:14:06 +0000 (19:14 +0200)
And since ClutterGestures will only be possible to implement in C (due to
performance concerns when we'll get rid of motion event compression and
start handling eg. 1000 HZ mouse events at the full rate), that means we
have to port the gesture to C!

It behaves mostly the same still: It's a single finger touch-only gesture
that recognizes as soon as a point that began on the monitor edge moves away
from that edge. It then emits "progress" signals with the distance from the
edge all the way until the fixed threshold distance (DRAG_DISTANCE) is
reached, where it then moves to COMPLETED.

There's definitely room for further improvements here, eg. the gesture
should probably only complete once the point is lifted from the screen.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2853>

js/js-resources.gresource.xml
js/ui/edgeDragAction.js [deleted file]
js/ui/keyboard.js
js/ui/windowManager.js
src/meson.build
src/shell-edge-drag-gesture.c [new file with mode: 0644]
src/shell-edge-drag-gesture.h [new file with mode: 0644]

index dc182cc748cf6aa08d0dd72620921d43e703aa85..e5e6167f150a7309db226abe9aca834ca1bb1391 100644 (file)
@@ -65,7 +65,6 @@
     <file>ui/dateMenu.js</file>
     <file>ui/dialog.js</file>
     <file>ui/dnd.js</file>
-    <file>ui/edgeDragAction.js</file>
     <file>ui/endSessionDialog.js</file>
     <file>ui/environment.js</file>
     <file>ui/extensionDownloader.js</file>
diff --git a/js/ui/edgeDragAction.js b/js/ui/edgeDragAction.js
deleted file mode 100644 (file)
index 99a3711..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-import Clutter from 'gi://Clutter';
-import GObject from 'gi://GObject';
-import Mtk from 'gi://Mtk';
-import St from 'gi://St';
-
-import * as Main from './main.js';
-
-const EDGE_THRESHOLD = 20;
-const DRAG_DISTANCE = 80;
-
-export const EdgeDragAction = GObject.registerClass({
-    Signals: {
-        'activated': {},
-        'progress': {param_types: [GObject.TYPE_DOUBLE]},
-    },
-}, class EdgeDragAction extends Clutter.GestureAction {
-    _init(side, allowedModes) {
-        super._init();
-        this._side = side;
-        this._allowedModes = allowedModes;
-        this.set_n_touch_points(1);
-        this.set_threshold_trigger_edge(Clutter.GestureTriggerEdge.AFTER);
-    }
-
-    _getMonitorRect(x, y) {
-        const rect = new Mtk.Rectangle({x: x - 1, y: y - 1, width: 1, height: 1});
-        let monitorIndex = global.display.get_monitor_index_for_rect(rect);
-
-        return global.display.get_monitor_geometry(monitorIndex);
-    }
-
-    vfunc_gesture_prepare(_actor) {
-        if (this.get_n_current_points() === 0)
-            return false;
-
-        if (!(this._allowedModes & Main.actionMode))
-            return false;
-
-        let [x, y] = this.get_press_coords(0);
-        let monitorRect = this._getMonitorRect(x, y);
-
-        return (this._side === St.Side.LEFT && x < monitorRect.x + EDGE_THRESHOLD) ||
-                (this._side === St.Side.RIGHT && x > monitorRect.x + monitorRect.width - EDGE_THRESHOLD) ||
-                (this._side === St.Side.TOP && y < monitorRect.y + EDGE_THRESHOLD) ||
-                (this._side === St.Side.BOTTOM && y > monitorRect.y + monitorRect.height - EDGE_THRESHOLD);
-    }
-
-    vfunc_gesture_progress(_actor) {
-        let [startX, startY] = this.get_press_coords(0);
-        let [x, y] = this.get_motion_coords(0);
-        let offsetX = Math.abs(x - startX);
-        let offsetY = Math.abs(y - startY);
-
-        if (offsetX < EDGE_THRESHOLD && offsetY < EDGE_THRESHOLD)
-            return true;
-
-        if ((offsetX > offsetY &&
-             (this._side === St.Side.TOP || this._side === St.Side.BOTTOM)) ||
-            (offsetY > offsetX &&
-             (this._side === St.Side.LEFT || this._side === St.Side.RIGHT))) {
-            this.cancel();
-            return false;
-        }
-
-        if (this._side === St.Side.TOP ||
-            this._side === St.Side.BOTTOM)
-            this.emit('progress', offsetY);
-        else
-            this.emit('progress', offsetX);
-
-        return true;
-    }
-
-    vfunc_gesture_end(_actor) {
-        let [startX, startY] = this.get_press_coords(0);
-        let [x, y] = this.get_motion_coords(0);
-        let monitorRect = this._getMonitorRect(startX, startY);
-
-        if ((this._side === St.Side.TOP && y > monitorRect.y + DRAG_DISTANCE) ||
-            (this._side === St.Side.BOTTOM && y < monitorRect.y + monitorRect.height - DRAG_DISTANCE) ||
-            (this._side === St.Side.LEFT && x > monitorRect.x + DRAG_DISTANCE) ||
-            (this._side === St.Side.RIGHT && x < monitorRect.x + monitorRect.width - DRAG_DISTANCE))
-            this.emit('activated');
-        else
-            this.cancel();
-    }
-});
index e3c8cd07a5267461095cc1958befb3bae4b0815b..47f0b00b392028d0a1f5a80c539df03c0ab392bd 100644 (file)
@@ -9,7 +9,6 @@ import Shell from 'gi://Shell';
 import St from 'gi://St';
 import * as Signals from '../misc/signals.js';
 
-import * as EdgeDragAction from './edgeDragAction.js';
 import * as InputSourceManager from './status/keyboard.js';
 import * as IBusManager from '../misc/ibusManager.js';
 import * as BoxPointer from './boxpointer.js';
@@ -1076,22 +1075,25 @@ export class KeyboardManager extends Signals.EventEmitter {
             this._syncEnabled();
         });
 
-        const mode = Shell.ActionMode.ALL & ~Shell.ActionMode.LOCK_SCREEN;
-        const bottomDragAction = new EdgeDragAction.EdgeDragAction(St.Side.BOTTOM, mode);
-        bottomDragAction.connect('activated', () => {
-            if (this._keyboard)
-                this._keyboard.gestureActivate(Main.layoutManager.bottomIndex);
+        const allowedModes = Shell.ActionMode.ALL & ~Shell.ActionMode.LOCK_SCREEN;
+        const bottomDragGesture = new Shell.EdgeDragGesture({
+            name: 'OSK show bottom drag',
+            side: St.Side.BOTTOM,
         });
-        bottomDragAction.connect('progress', (_action, progress) => {
-            if (this._keyboard)
-                this._keyboard.gestureProgress(progress);
+        bottomDragGesture.connect('may-recognize', () => {
+            return allowedModes & Main.actionMode;
         });
-        bottomDragAction.connect('gesture-cancel', () => {
-            if (this._keyboard)
-                this._keyboard.gestureCancel();
+        bottomDragGesture.connect('progress', (_action, progress) => {
+            this._keyboard?.gestureProgress(progress);
         });
-        global.stage.add_action_full('osk', Clutter.EventPhase.CAPTURE, bottomDragAction);
-        this._bottomDragAction = bottomDragAction;
+        bottomDragGesture.connect('end', () => {
+            this._keyboard?.gestureActivate(Main.layoutManager.bottomIndex);
+        });
+        bottomDragGesture.connect('cancel', () => {
+            this._keyboard?.gestureCancel();
+        });
+        global.stage.add_action(bottomDragGesture);
+        this._bottomDragGesture = bottomDragGesture;
 
         this._syncEnabled();
     }
@@ -1116,13 +1118,13 @@ export class KeyboardManager extends Signals.EventEmitter {
             this._keyboard = new Keyboard();
             this._keyboard.connect('visibility-changed', () => {
                 this.emit('visibility-changed');
-                this._bottomDragAction.enabled = !this._keyboard.visible;
+                this._bottomDragGesture.enabled = !this._keyboard.visible;
             });
         } else if (!enabled && this._keyboard) {
             this._keyboard.setCursorLocation(null);
             this._keyboard.destroy();
             this._keyboard = null;
-            this._bottomDragAction.enabled = true;
+            this._bottomDragGesture.enabled = true;
         }
     }
 
index a9936c53011bf2194ef2c7cb156071512593b5bc..d654228fc0e80ee3d609cc9fcab7dc4a3013557e 100644 (file)
@@ -16,7 +16,6 @@ import * as InhibitShortcutsDialog from './inhibitShortcutsDialog.js';
 import * as ModalDialog from './modalDialog.js';
 import * as WindowMenu from './windowMenu.js';
 import * as PadOsd from './padOsd.js';
-import * as EdgeDragAction from './edgeDragAction.js';
 import * as CloseDialog from './closeDialog.js';
 import * as SwitchMonitor from './switchMonitor.js';
 import * as IBusManager from '../misc/ibusManager.js';
@@ -911,9 +910,15 @@ export class WindowManager {
         if (Main.sessionMode.hasWorkspaces)
             this._workspaceTracker = new WorkspaceTracker(this);
 
-        let mode = Shell.ActionMode.NORMAL;
-        let topDragAction = new EdgeDragAction.EdgeDragAction(St.Side.TOP, mode);
-        topDragAction.connect('activated',  () => {
+        const allowedModes = Shell.ActionMode.NORMAL;
+        const topDragGesture = new Shell.EdgeDragGesture({
+            name: 'Window unfullscreen top drag',
+            side: St.Side.TOP,
+        });
+        topDragGesture.connect('may-recognize', () => {
+            return allowedModes & Main.actionMode;
+        });
+        topDragGesture.connect('end', () => {
             let currentWindow = global.display.focus_window;
             if (currentWindow)
                 currentWindow.unmake_fullscreen();
@@ -921,14 +926,14 @@ export class WindowManager {
 
         let updateUnfullscreenGesture = () => {
             let currentWindow = global.display.focus_window;
-            topDragAction.enabled = currentWindow && currentWindow.is_fullscreen();
+            topDragGesture.enabled = currentWindow && currentWindow.is_fullscreen();
         };
 
         global.display.connect('notify::focus-window', updateUnfullscreenGesture);
         global.display.connect('in-fullscreen-changed', updateUnfullscreenGesture);
         updateUnfullscreenGesture();
 
-        global.stage.add_action_full('unfullscreen', Clutter.EventPhase.CAPTURE, topDragAction);
+        global.stage.add_action(topDragGesture);
 
         this._workspaceAnimation =
             new WorkspaceAnimation.WorkspaceAnimationController();
index e16757a4054175bdd8e8502a062e9d0da13f2ba5..b729ec03a933799162aa018158d9aeffd02f0b3c 100644 (file)
@@ -113,6 +113,7 @@ libshell_public_headers = [
   'shell-app-usage.h',
   'shell-blur-effect.h',
   'shell-camera-monitor.h',
+  'shell-edge-drag-gesture.h',
   'shell-glsl-effect.h',
   'shell-global.h',
   'shell-invert-lightness-effect.h',
@@ -164,6 +165,7 @@ libshell_sources = [
   'shell-app-usage.c',
   'shell-blur-effect.c',
   'shell-camera-monitor.c',
+  'shell-edge-drag-gesture.c',
   'shell-global.c',
   'shell-glsl-effect.c',
   'shell-invert-lightness-effect.c',
diff --git a/src/shell-edge-drag-gesture.c b/src/shell-edge-drag-gesture.c
new file mode 100644 (file)
index 0000000..a2a84fd
--- /dev/null
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2025 Jonas Dreßler <verdre@v0yd.nl>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include <clutter/clutter.h>
+
+#include "shell-edge-drag-gesture.h"
+#include "shell-global.h"
+
+#define EDGE_THRESHOLD 20
+#define DRAG_DISTANCE 80
+
+struct _ShellEdgeDragGesture
+{
+  ClutterGesture parent;
+
+  StSide side;
+};
+
+enum
+{
+  PROP_0,
+
+  PROP_SIDE,
+
+  PROP_LAST
+};
+
+enum
+{
+  PROGRESS,
+
+  LAST_SIGNAL
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+static unsigned int obj_signals[LAST_SIGNAL] = { 0, };
+
+G_DEFINE_FINAL_TYPE (ShellEdgeDragGesture, shell_edge_drag_gesture, CLUTTER_TYPE_GESTURE);
+
+static gboolean
+get_monitor_for_coords (ShellEdgeDragGesture *self,
+                        graphene_point_t     *coords,
+                        MtkRectangle         *monitor_geometry_out)
+{
+  MetaDisplay *display = shell_global_get_display (shell_global_get ());
+  unsigned int monitor_index;
+  MtkRectangle rect;
+
+  rect.x = coords->x - 1;
+  rect.y = coords->y - 1;
+  rect.width = 1;
+  rect.height = 1;
+
+  monitor_index = meta_display_get_monitor_index_for_rect (display, &rect);
+  if (monitor_index == -1)
+    return FALSE;
+
+  meta_display_get_monitor_geometry (display, monitor_index, monitor_geometry_out);
+  return TRUE;
+}
+
+static gboolean
+is_near_monitor_edge (ShellEdgeDragGesture *self,
+                      unsigned int          point)
+{
+  graphene_point_t coords;
+  MtkRectangle monitor_geometry;
+
+  clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self),
+                                        point,
+                                        &coords);
+
+  if (!get_monitor_for_coords (self, &coords, &monitor_geometry))
+    {
+      g_assert_not_reached ();
+      return FALSE;
+    }
+
+  switch (self->side)
+    {
+    case ST_SIDE_LEFT:
+      return coords.x < (float) (monitor_geometry.x + EDGE_THRESHOLD);
+    case ST_SIDE_RIGHT:
+      return coords.x > (float) (monitor_geometry.x + monitor_geometry.width - EDGE_THRESHOLD);
+    case ST_SIDE_TOP:
+      return coords.y < (float) (monitor_geometry.y + EDGE_THRESHOLD);
+    case ST_SIDE_BOTTOM:
+      return coords.y > (float) (monitor_geometry.y + monitor_geometry.height - EDGE_THRESHOLD);
+    }
+
+  g_assert_not_reached ();
+  return FALSE;
+}
+
+static gboolean
+exceeds_cancel_threshold (ShellEdgeDragGesture *self,
+                          unsigned int          point)
+{
+  graphene_point_t begin_coords, latest_coords;
+  float distance_x, distance_y;
+
+  clutter_gesture_get_point_begin_coords_abs (CLUTTER_GESTURE (self),
+                                              point,
+                                              &begin_coords);
+
+  clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self),
+                                        point,
+                                        &latest_coords);
+  
+  graphene_point_distance (&latest_coords, &begin_coords, &distance_x, &distance_y);
+
+  switch (self->side)
+    {
+    case ST_SIDE_LEFT:
+    case ST_SIDE_RIGHT:
+      return distance_y > distance_x;
+
+    case ST_SIDE_TOP:
+    case ST_SIDE_BOTTOM:
+      return distance_x > distance_y;
+    }
+
+  g_assert_not_reached ();
+  return FALSE;
+}
+
+static gboolean
+passes_distance_needed (ShellEdgeDragGesture *self,
+                        unsigned int          sequence)
+{
+  graphene_point_t begin_coords, latest_coords;
+  MtkRectangle monitor_geometry;
+
+  clutter_gesture_get_point_begin_coords_abs (CLUTTER_GESTURE (self),
+                                              sequence,
+                                              &begin_coords);
+
+  if (!get_monitor_for_coords (self, &begin_coords, &monitor_geometry))
+    {
+      g_assert_not_reached ();
+      return FALSE;
+    }
+
+  clutter_gesture_get_point_coords_abs (CLUTTER_GESTURE (self),
+                                        sequence,
+                                        &latest_coords);
+
+  switch (self->side)
+    {
+    case ST_SIDE_LEFT:
+      return latest_coords.x > (float) (monitor_geometry.x + DRAG_DISTANCE);
+    case ST_SIDE_RIGHT:
+      return latest_coords.x < (float) (monitor_geometry.x + monitor_geometry.width - DRAG_DISTANCE);
+    case ST_SIDE_TOP:
+      return latest_coords.y > (float) (monitor_geometry.y + DRAG_DISTANCE);
+    case ST_SIDE_BOTTOM:
+      return latest_coords.y < (float) (monitor_geometry.y + monitor_geometry.height - DRAG_DISTANCE);
+    }
+
+  g_assert_not_reached ();
+  return FALSE;
+}
+
+static gboolean
+shell_edge_drag_gesture_should_handle_sequence (ClutterGesture     *gesture,
+                                                const ClutterEvent *sequence_begin_event)
+{
+  ClutterEventType event_type = clutter_event_type (sequence_begin_event);
+
+  if (event_type == CLUTTER_TOUCH_BEGIN)
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+shell_edge_drag_gesture_point_began (ClutterGesture *gesture,
+                                     unsigned int    point)
+{
+  ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gesture);
+  unsigned int n_points = clutter_gesture_get_n_points (gesture);
+
+  if (n_points > 1 ||
+      !is_near_monitor_edge (self, point))
+    {
+      clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED);
+      return;
+    }
+}
+
+static void
+shell_edge_drag_gesture_point_moved (ClutterGesture *gesture,
+                                     unsigned int    point)
+{
+  ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gesture);
+
+  if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_POSSIBLE &&
+      is_near_monitor_edge (self, point))
+    return;
+
+  if (exceeds_cancel_threshold (self, point))
+    {
+      clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED);
+      return;
+    }
+
+  if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_POSSIBLE)
+    clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_RECOGNIZING);
+
+  if (clutter_gesture_get_state (gesture) == CLUTTER_GESTURE_STATE_RECOGNIZING)
+    {
+      graphene_point_t begin_coords, latest_coords;
+      float distance_x, distance_y;
+
+      clutter_gesture_get_point_begin_coords_abs (gesture,
+                                                  point,
+                                                  &begin_coords);
+
+      clutter_gesture_get_point_coords_abs (gesture,
+                                            point,
+                                            &latest_coords);
+
+      graphene_point_distance (&latest_coords, &begin_coords, &distance_x, &distance_y);
+
+      switch (self->side)
+        {
+        case ST_SIDE_LEFT:
+        case ST_SIDE_RIGHT:
+          g_signal_emit (self, obj_signals[PROGRESS], 0, distance_x);
+          break;
+
+        case ST_SIDE_TOP:
+        case ST_SIDE_BOTTOM:
+          g_signal_emit (self, obj_signals[PROGRESS], 0, distance_y);
+          break;
+        }
+
+      if (passes_distance_needed (self, point))
+        clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_COMPLETED);
+    }
+}
+
+static void
+shell_edge_drag_gesture_point_ended (ClutterGesture *gesture,
+                                     unsigned int    point)
+{
+  clutter_gesture_set_state (gesture, CLUTTER_GESTURE_STATE_CANCELLED);
+}
+
+static void
+shell_edge_drag_gesture_set_property (GObject      *gobject,
+                                      unsigned int  prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_SIDE:
+      shell_edge_drag_gesture_set_side (self, g_value_get_enum (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+shell_edge_drag_gesture_get_property (GObject      *gobject,
+                                      unsigned int  prop_id,
+                                      GValue       *value,
+                                      GParamSpec   *pspec)
+{
+  ShellEdgeDragGesture *self = SHELL_EDGE_DRAG_GESTURE (gobject);
+
+  switch (prop_id)
+    {
+    case PROP_SIDE:
+      g_value_set_enum (value, shell_edge_drag_gesture_get_side (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
+      break;
+    }
+}
+
+static void
+shell_edge_drag_gesture_init (ShellEdgeDragGesture *self)
+{
+  self->side = ST_SIDE_TOP;
+}
+
+static void
+shell_edge_drag_gesture_class_init (ShellEdgeDragGestureClass *klass)
+{
+  ClutterGestureClass *gesture_class = CLUTTER_GESTURE_CLASS (klass);
+  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+  gesture_class->should_handle_sequence = shell_edge_drag_gesture_should_handle_sequence;
+  gesture_class->point_began = shell_edge_drag_gesture_point_began;
+  gesture_class->point_moved = shell_edge_drag_gesture_point_moved;
+  gesture_class->point_ended = shell_edge_drag_gesture_point_ended;
+
+  gobject_class->set_property = shell_edge_drag_gesture_set_property;
+  gobject_class->get_property = shell_edge_drag_gesture_get_property;
+
+  /**
+   * ShellEdgeDragGesture:side:
+   *
+   * Edge that the gesture may start at. Defaults to the top edge.
+   */
+  obj_props[PROP_SIDE] =
+    g_param_spec_enum ("side", NULL, NULL,
+                       CLUTTER_TYPE_GESTURE_STATE,
+                       ST_SIDE_TOP,
+                       G_PARAM_READWRITE |
+                       G_PARAM_STATIC_STRINGS |
+                       G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (gobject_class, PROP_LAST, obj_props);
+
+  /**
+   * ShellEdgeDragGesture::progress:
+   * @gesture: the #ShellEdgeDragGesture that emitted the signal
+   * @progress_px: the progress of the gesture in pixels in the selected direction
+   *
+   * The ::progress signal is emitted when the edge drag has moved
+   */
+  obj_signals[PROGRESS] =
+    g_signal_new ("progress",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1,
+                  G_TYPE_FLOAT);
+}
+
+/**
+ * shell_edge_drag_gesture_set_side:
+ * @self: a #ShellEdgeDragGesture
+ * @side: the side
+ *
+ * Sets the edge of the monitor that the edge drag may start at.
+ */
+void
+shell_edge_drag_gesture_set_side (ShellEdgeDragGesture *self,
+                                  StSide                side)
+{
+  g_return_if_fail (SHELL_IS_EDGE_DRAG_GESTURE (self));
+  g_return_if_fail (side >= ST_SIDE_TOP && side <= ST_SIDE_LEFT);
+
+  if (self->side == side)
+    return;
+
+  self->side = side;
+
+  g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_SIDE]);
+}
+
+/**
+ * shell_edge_drag_gesture_get_side:
+ * @self: a #ShellEdgeDragGesture
+ *
+ * Gets the edge of the monitor that the edge drag may start at.
+ *
+ * Returns: the side that the edge drag may start at
+ */
+StSide
+shell_edge_drag_gesture_get_side (ShellEdgeDragGesture *self)
+{
+  g_return_val_if_fail (SHELL_IS_EDGE_DRAG_GESTURE (self), ST_SIDE_TOP);
+
+  return self->side;
+}
diff --git a/src/shell-edge-drag-gesture.h b/src/shell-edge-drag-gesture.h
new file mode 100644 (file)
index 0000000..3acf432
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2025 Jonas Dreßler <verdre@v0yd.nl>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#include <clutter/clutter.h>
+#include <st/st.h>
+
+#define SHELL_TYPE_EDGE_DRAG_GESTURE (shell_edge_drag_gesture_get_type ())
+G_DECLARE_FINAL_TYPE (ShellEdgeDragGesture, shell_edge_drag_gesture,
+                      SHELL, EDGE_DRAG_GESTURE, ClutterGesture)
+
+void shell_edge_drag_gesture_set_side (ShellEdgeDragGesture *self,
+                                       StSide                side);
+
+StSide shell_edge_drag_gesture_get_side (ShellEdgeDragGesture *self);
+
+G_END_DECLS